/**
 * validationUtils.ts
 * Contains utility functions for XML validation, especially using libxml2-wasm with schemas.
 */

import * as monaco from 'monaco-editor';
// Import necessary classes and functions from libxml2-wasm
import { XmlDocument, XsdValidator, xmlRegisterInputProvider } from 'libxml2-wasm';

// IMPORTANT: The webFetchInputProvider needs to be registered *once* during app initialization.
// Add the following lines to your main.ts or a suitable init function:
// import { xmlRegisterInputProvider } from 'libxml2-wasm';
// import { webFetchInputProvider } from '@/utils/validationUtils'; // Adjust path if needed
// xmlRegisterInputProvider(webFetchInputProvider);
// console.log("[Main] Registered webFetchInputProvider for libxml2.");

// Define an interface for schema details
export interface SchemaInfo { // Export interface if needed elsewhere
    type: 'jdf' | 'xjdf' | 'ptk' | 'none';
    version: string | null;
}

// Cache for loaded schema documents to avoid re-fetching and re-parsing
// Key: schema path (e.g., "/schemas/jdf/1.8/JDF.xsd"), Value: Promise<XmlDocument | null>
// Store the promise to handle concurrent requests for the same schema
const schemaCache = new Map<string, Promise<XmlDocument | null>>();

/**
 * Fetches and parses a schema file, utilizing a cache.
 * Disposes the created schema document if parsing fails during the fetch operation.
 * @param schemaPath Path to the schema file in the public directory.
 * @returns A Promise resolving to the parsed XmlDocument or null if fetching/parsing fails.
 */
async function loadAndParseSchema(schemaPath: string): Promise<XmlDocument | null> {
    console.log(`[SchemaLoader] Request to load schema: ${schemaPath}`);
    if (schemaCache.has(schemaPath)) {
        console.log(`[SchemaCache] Cache hit for ${schemaPath}`);
        // Return the promise which resolves to the cached document (or null if initial load failed)
        return schemaCache.get(schemaPath)!;
    }

    console.log(`[SchemaCache] Cache miss for ${schemaPath}, initiating fetch...`);
    const loadPromise = (async () => {
        let schemaDoc: XmlDocument | null = null;
        try {
            const response = await fetch(schemaPath);
            console.log(`[SchemaLoader] Fetched ${schemaPath}, Status: ${response.status}`);
            if (!response.ok) {
                throw new Error(`Failed to fetch schema ${schemaPath}: ${response.statusText}`);
            }
            const schemaContent = await response.text();
            console.log(`[SchemaLoader] Read ${schemaContent.length} chars for ${schemaPath}`);
            // Parse the schema content
            schemaDoc = XmlDocument.fromString(schemaContent);
            console.log(`[SchemaLoader] Successfully parsed schema: ${schemaPath}. Document object created.`);
            // IMPORTANT: Do NOT dispose the schemaDoc here if successful, as it's meant to be cached/used.
            return schemaDoc;
        } catch (error) {
            console.error(`[SchemaLoader] Error loading/parsing schema ${schemaPath}:`, error);
            // If schemaDoc was created but parsing failed later (less likely with fromString),
            // or if an error occurred after creation, attempt disposal.
            if (schemaDoc && typeof (schemaDoc as any).dispose === 'function') {
                try {
                    (schemaDoc as any).dispose();
                    console.warn(`[SchemaLoader] Disposed partially created/failed schema doc for ${schemaPath}`);
                } catch(e) {/*ignore dispose error*/}
            }
            return null; // Indicate failure by resolving promise to null
        }
    })();

    // Store the promise in the cache
    schemaCache.set(schemaPath, loadPromise);
    return loadPromise;
}

// --- Virtual I/O Provider for fetching included/imported schemas --- 
// This object needs to be registered using xmlRegisterInputProvider during app init.
export const webFetchInputProvider = {
    // Match URLs starting with /schemas/ or file:///schemas/ (adjust if needed)
    match(url: string): boolean {
        console.log(`[InputProvider] match called with URL: "${url}"`);
        const validPrefix = '/schemas/';
        const filePrefix = 'file:///schemas/'; // Might be passed if base URL is file://
        const isMatch = url.startsWith(validPrefix) || url.startsWith(filePrefix);
        console.log(`[InputProvider] match result for "${url}": ${isMatch}`);
        return isMatch;
    },

    // open: Fetch data and return a context object { buffer, position, url }
    async open(url: string): Promise<{ buffer: Uint8Array; position: number; url: string } | null> {
        console.log(`[InputProvider] open requested for URL: "${url}"`);
        try {
            // Ensure we fetch using the correct absolute path from the root
            let fetchUrl = url;
            if (url.startsWith('file://')) {
                 console.log(`[InputProvider] URL starts with file://, stripping prefix.`);
                 fetchUrl = url.substring('file://'.length); // Remove file:// prefix
            }
            // Important: Ensure the path is absolute relative to the web server root
            if (!fetchUrl.startsWith('/')) {
                console.warn(`[InputProvider] Relative fetch URL detected: "${fetchUrl}". Assuming it's relative to root.`);
                // This might need adjustment based on where libxml2 thinks the base is.
                // For now, let's prepend '/' if missing, assuming it means root relative.
                 fetchUrl = '/' + fetchUrl;
            }

            console.log(`[InputProvider] Attempting to fetch absolute URL: "${fetchUrl}"`);

            const response = await fetch(fetchUrl);
            console.log(`[InputProvider] Fetch status for "${fetchUrl}": ${response.status}`);
            if (!response.ok) {
                throw new Error(`HTTP error! status: ${response.status} for ${fetchUrl}`);
            }
            const buffer = await response.arrayBuffer();
            console.log(`[InputProvider] Fetched ${buffer.byteLength} bytes for ${fetchUrl}. Preparing context.`);
            return { buffer: new Uint8Array(buffer), position: 0, url: fetchUrl };
        } catch (error) {
            console.error(`[InputProvider] Failed to fetch or read ${url}:`, error);
            // Return null or throw to indicate failure to libxml2?
            // Let's return null, libxml2 should handle the load failure.
            return null; 
        }
    },

    // read: Copy data from our fetched buffer to libxml2's buffer
    read(context: any, buffer: Uint8Array, length: number): number {
        if (!context || !context.buffer) {
            console.error(`[InputProvider] read called with invalid context for url: ${context?.url}`);
            return -1; // Error
        }
        // console.log(`[InputProvider] read called for: ${context.url}, requested: ${length}, current pos: ${context.position}`);
        const bytesAvailable = context.buffer.length - context.position;
        const bytesToCopy = Math.min(length, bytesAvailable);

        if (bytesToCopy <= 0) {
            // console.log(`[InputProvider] Read EOF for: ${context.url}`);
            return 0; // End of file
        }

        try {
            buffer.set(context.buffer.subarray(context.position, context.position + bytesToCopy));
            context.position += bytesToCopy;
            // console.log(`[InputProvider] Read ${bytesToCopy} bytes for: ${context.url}, new pos: ${context.position}`);
            return bytesToCopy; // Return number of bytes read
        } catch (error) {
            console.error(`[InputProvider] Error during read buffer.set for ${context.url}:`, error);
            return -1; // Indicate error
        }
    },

    // close: No specific cleanup needed for ArrayBuffer context
    close(context: any): number {
        // console.log(`[InputProvider] close called for: ${context?.url}`);
        return 0; // Success
    }
};

/**
 * Validates XML content using libxml2-wasm, optionally against an XSD schema.
 * Handles XML parsing, schema loading (with cache), XSD validation, and error conversion.
 * Ensures proper disposal of the XML document object created from xmlContent.
 * Schema documents loaded from cache are NOT disposed here.
 *
 * @param xmlContent The XML string content to validate.
 * @param schemaInfo Object containing schema type and version, or type 'none'.
 * @returns A Promise resolving to an array of Monaco Editor markers.
 */
export async function validateWithLibxml2Schema(
    xmlContent: string,
    schemaInfo: SchemaInfo
): Promise<monaco.editor.IMarkerData[]> {
    let markers: monaco.editor.IMarkerData[] = [];
    let doc: XmlDocument | null = null;
    let schemaDocFromCache: XmlDocument | null = null; // Hold the resolved schema doc
    let validator: XsdValidator | null = null; // Use specific type

    try {
        // --- Step 1: Parse the XML content (Syntax Check) ---
        try {
            doc = XmlDocument.fromString(xmlContent);
            console.log('[libxml2-wasm] Content parsed successfully (Syntax check).');
        } catch (parseError: any) {
            console.error('[libxml2-wasm] XML content parsing error:', parseError);
            markers = [convertLibXmlErrorToMarker(parseError, 'Parse Error')];
            // If syntax fails, no point in schema validation, return syntax error marker
            // Dispose doc if it was partially created (unlikely but possible)
            if (doc && typeof (doc as any).dispose === 'function') {
                try { (doc as any).dispose(); } catch (e) {/*ignore*/}
            }
            return markers;
        }

        // --- Step 2: Schema Validation (if requested) ---
        if (schemaInfo.type !== 'none' && schemaInfo.version) {
            const schemaBasePath = `/schemas/${schemaInfo.type}/${schemaInfo.version}/`;
            const mainSchemaFileName = schemaInfo.type === 'jdf' ? 'JDF.xsd'
                : schemaInfo.type === 'xjdf' ? 'xjdf.xsd'
                    : 'PrintTalk.xsd'; // Assuming 'ptk' uses PrintTalk.xsd
            const schemaPath = `${schemaBasePath}${mainSchemaFileName}`;

            console.log(`[libxml2-wasm] Attempting schema validation with: ${schemaPath}`);

            // Load and parse the schema (potentially from cache)
            schemaDocFromCache = await loadAndParseSchema(schemaPath);

            if (!schemaDocFromCache) {
                // Failed to load/parse schema, create a general marker
                markers.push({
                    message: `[libxml2-wasm] Failed to load or parse schema: ${schemaPath}`,
                    severity: monaco.MarkerSeverity.Warning,
                    startLineNumber: 1, startColumn: 1, endLineNumber: 1, endColumn: 2
                });
                // Continue without schema validation, markers array now contains the loading error
            } else {
                // Schema loaded successfully, create validator and validate
                console.log(`[libxml2-wasm] Schema doc ${schemaPath} loaded successfully from cache/fetch. Attempting to create validator...`);
                try {
                    // Create validator from the successfully loaded schema document
                    validator = XsdValidator.fromDoc(schemaDocFromCache);
                    console.log('[libxml2-wasm] XsdValidator created successfully.');

                    // Perform validation - this is expected to throw errors on failure
                    console.log('[libxml2-wasm] Attempting validation against parsed XML document...');
                    validator.validate(doc);
                    console.log('[libxml2-wasm] Schema validation successful (validate() did not throw).');
                    // If validate() does not throw, validation passed, markers remain empty (or contain only schema load warnings)

                } catch (validationError: any) {
                    console.error('[libxml2-wasm] Schema validation error:', validationError);
                    // Validation failed, convert the single error object to a marker
                    markers = [convertLibXmlErrorToMarker(validationError, 'Schema Validation')];
                } finally {
                    // Dispose the validator immediately after use
                    if (validator && typeof validator.dispose === 'function') {
                        try { validator.dispose(); } catch (e) { console.error('Error disposing validator:', e); }
                    }
                    // Do NOT dispose schemaDocFromCache here - it might be needed by other concurrent validations
                }
            }
        } else {
            console.log('[libxml2-wasm] No schema selected, skipping schema validation.');
            // Syntax check passed, no schema validation requested, markers remain empty.
        }

    } catch (generalError: any) {
        // Catch any unexpected errors during the process (outside specific try-catches)
        console.error('[libxml2-wasm] Unexpected validation process error:', generalError);
        markers = [{
            message: `[libxml2-wasm] Unexpected Error: ${generalError.message || 'Unknown error'}`,
            severity: monaco.MarkerSeverity.Error,
            startLineNumber: 1, startColumn: 1, endLineNumber: 1, endColumn: 2
        }];
    } finally {
        // --- Step 3: Dispose the XML document object created from xmlContent ---
        if (doc && typeof (doc as any).dispose === 'function') {
            try {
                (doc as any).dispose();
                console.log('[libxml2-wasm] Disposed XML content document.');
            } catch (e) { console.error('Error disposing XML doc:', e); }
        }
    }

    // Return the array of markers (could be empty if validation passed)
    return markers;
}


/**
 * Helper function to convert a libxml2 error object into a Monaco Marker.
 * @param error The error object thrown by libxml2-wasm.
 * @param errorType A string prefix like 'Parse Error' or 'Schema Validation'.
 * @returns A monaco.editor.IMarkerData object.
 */
function convertLibXmlErrorToMarker(error: any, errorType: string = 'Error'): monaco.editor.IMarkerData {
    let line = 1;
    let column = 1;
    let message = `Unknown ${errorType}`;
    let severity = monaco.MarkerSeverity.Error; // Default to Error

    // Attempt to extract info based on observed error structure or documented properties
    if (error) {
        // Use direct properties if they exist
        line = error.line || line;
        column = error.column || column;

        if (error.message && typeof error.message === 'string') {
            message = error.message.split('\n')[0]; // Get first line
            // Attempt to parse line/col from message string if direct properties missing
            if (!error.line) {
                const lineMatch = error.message.match(/line[=\s:]+(\d+)/i);
                if (lineMatch) line = parseInt(lineMatch[1], 10);
            }
            if (!error.column) {
                const colMatch = error.message.match(/column[=\s:]+(\d+)/i);
                if (colMatch) column = parseInt(colMatch[1], 10);
            }
        } else if (typeof error === 'string') {
            message = error.split('\n')[0];
        }
        // Add code/level if available
        const code = error.code ? ` (Code ${error.code})` : '';
        const level = error.level; // Check for level (might indicate warning vs error)

        // Adjust severity based on level? (libxml2 error levels might map)
        // Example: Level 1 might be Warning, Level 2+ Error. Needs confirmation.
        if (level === 1) { // Assuming 1 is Warning
            severity = monaco.MarkerSeverity.Warning;
        }

        message = `[libxml2-wasm ${errorType}]${level ? ` (Lvl ${level})` : ''}${code} ${message}`;
    } else {
        message = `[libxml2-wasm] ${errorType}: An unknown error occurred.`;
    }

    // Ensure line/column are valid numbers
    line = Math.max(1, Number.isFinite(line) ? line : 1);
    column = Math.max(1, Number.isFinite(column) ? column : 1);

    return {
        message,
        severity,
        startLineNumber: line,
        startColumn: column,
        // Try to make the end column slightly wider if possible, otherwise just use start column
        endLineNumber: line,
        endColumn: column > 1 ? column + 1 : 2
    };
}